<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<title>梦境射击游戏 - 强化版</title>
<style>
  :root {
    --ui-bg: rgba(20,20,20,0.6);
    --ui-border: rgba(255,255,255,0.15);
    --accent: #ffd24a;
    --hp: #ff4d4f;
    --xp: #4CAF50;
    --text: #f2f2f2;
  }
  html, body {
    margin: 0; padding: 0; background: #111; color: var(--text);
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
    height: 100%;
    overflow: hidden;
  }
  #wrap {
    position: relative; width: 100vw; height: 100vh;
    display: grid; place-items: center;
  }
  canvas#game {
    width: min(96vw, 1000px);
    height: min(70vh, 640px);
    border: 2px solid #333; background: #1f1f1f; border-radius: 8px;
    touch-action: none;
  }
  /* 顶部HUD */
  #hud {
    position: absolute; top: 12px; left: 50%; transform: translateX(-50%);
    width: min(96vw, 1000px);
    display: flex; gap: 12px; align-items: center; justify-content: space-between;
    pointer-events: none;
  }
  .panel {
    background: var(--ui-bg);
    border: 1px solid var(--ui-border);
    border-radius: 10px;
    padding: 8px 12px;
    backdrop-filter: blur(6px);
  }
  #leftHUD {
    display: flex; gap: 10px; align-items: center;
  }
  .bar {
    width: 240px; height: 14px; background: rgba(255,255,255,0.08); border-radius: 7px; overflow: hidden; border: 1px solid var(--ui-border);
  }
  .bar .fill { height: 100%; width: 0%; }
  .bar.hp .fill { background: linear-gradient(90deg, #ff7875, var(--hp)); }
  .bar.xp .fill { background: linear-gradient(90deg, #69db7c, var(--xp)); }
  .label { font-size: 13px; opacity: 0.9; margin-bottom: 4px; }
  .group { display: grid; gap: 4px; }
  #rightHUD {
    display: flex; gap: 10px; align-items: center;
  }
  #waveBox {
    display: grid; gap: 4px;
  }
  #waveTitle { font-weight: 600; }
  #coopTag {
    padding: 4px 8px; font-size: 12px; border-radius: 999px;
    background: rgba(255,210,74,0.12); border: 1px solid rgba(255,210,74,0.4);
    color: var(--accent); text-shadow: 0 0 6px rgba(255,210,74,0.4);
  }
  /* 底部技能冷却 */
  #skills {
    position: absolute; bottom: 14px; left: 50%; transform: translateX(-50%);
    display: flex; gap: 16px; pointer-events: none;
  }
  .skill {
    width: 64px; height: 64px; border-radius: 50%;
    background: radial-gradient(ellipse at center, rgba(255,255,255,0.08), rgba(255,255,255,0.03));
    border: 1px solid var(--ui-border); position: relative; box-shadow: inset 0 0 0 2px rgba(0,0,0,0.2);
  }
  .skill .name {
    position: absolute; bottom: -18px; width: 100%; text-align: center; font-size: 12px; opacity: 0.75;
  }
  .skill .cd {
    position: absolute; inset: 0; border-radius: 50%;
    background: conic-gradient(#ff695e 0%, rgba(255,255,255,0.08) 0%);
    display: none;
  }
  .skill .sec {
    position: absolute; inset: 0; display: grid; place-items: center; font-weight: 700; font-size: 18px;
  }
  /* 中央消息/弹窗 */
  #toast {
    position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
    font-size: 40px; font-weight: 800; color: white;
    text-shadow: 0 8px 24px rgba(0,0,0,0.6), 0 0 20px rgba(255,255,255,0.2);
    opacity: 0; transition: opacity 250ms ease, transform 250ms ease;
    pointer-events: none;
  }
  #toast.show {
    opacity: 1; transform: translate(-50%,-55%);
  }
  #upgradeModal, #gameOver {
    position: absolute; inset: 0; display: none; place-items: center; background: rgba(0,0,0,0.45);
  }
  .dialog {
    width: min(92vw, 700px);
    background: #202226; border: 1px solid var(--ui-border); border-radius: 14px; padding: 18px;
    box-shadow: 0 12px 40px rgba(0,0,0,0.45);
  }
  .dialog h2 { margin: 6px 0 16px 0; }
  .choices { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit,minmax(180px,1fr)); }
  .choice {
    background: linear-gradient(180deg, rgba(255,255,255,0.09), rgba(255,255,255,0.03));
    border: 1px solid var(--ui-border); border-radius: 10px; padding: 12px; cursor: pointer;
    transition: transform 0.08s ease;
  }
  .choice:hover { transform: translateY(-2px); }
  .choice .t { font-weight: 700; margin-bottom: 6px; color: var(--accent); }
  #gameOver .dialog { text-align: center; }
  .btn {
    margin-top: 14px; padding: 10px 16px; border: 0; border-radius: 8px; background: var(--accent); color: #222; font-weight: 700; cursor: pointer;
  }
</style>
</head>
<body>
<div id="wrap">
  <canvas id="game" width="960" height="600"></canvas>

  <!-- 顶部HUD -->
  <div id="hud">
    <div id="leftHUD" class="panel">
      <div class="group">
        <div class="label">生命</div>
        <div class="bar hp"><div id="hpFill" class="fill"></div></div>
      </div>
      <div class="group" style="min-width:220px;">
        <div class="label">经验 Lv.<span id="lv">1</span></div>
        <div class="bar xp"><div id="xpFill" class="fill"></div></div>
      </div>
      <div class="group">
        <div class="label">分数</div>
        <div id="score" style="font-weight:700;">0</div>
      </div>
    </div>

    <div id="rightHUD" class="panel">
      <div id="waveBox">
        <div id="waveTitle">第 <span id="waveNo">1</span> 波</div>
        <div id="waveInfo" style="font-size:12px; opacity:0.85;">剩余敌人 <span id="left">0</span></div>
      </div>
      <div id="coopTag" style="display:none;">协同加成</div>
    </div>
  </div>

  <!-- 底部技能冷却 -->
  <div id="skills">
    <div class="skill" id="skA">
      <div class="cd"></div>
      <div class="sec"></div>
      <div class="name">单击 射击</div>
    </div>
    <div class="skill" id="skB">
      <div class="cd"></div>
      <div class="sec"></div>
      <div class="name">双击 范围</div>
    </div>
  </div>

  <div id="toast">LV UP!</div>

  <!-- 升级选择 -->
  <div id="upgradeModal">
    <div class="dialog">
      <h2>升级选择</h2>
      <div class="choices" id="choices"></div>
    </div>
  </div>

  <!-- 游戏结束 -->
  <div id="gameOver">
    <div class="dialog">
      <h2 style="font-size:40px; margin: 0 0 10px 0;">游戏结束</h2>
      <div style="opacity:0.85">本次分数：<span id="finalScore">0</span></div>
      <button class="btn" id="restartBtn">重新开始</button>
      <div style="font-size:12px; opacity:0.7; margin-top:6px;">提示：靠近蓝色AI获得加成，单击/双击释放技能。</div>
    </div>
  </div>
</div>

<script>
(() => {
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');

  // 简易自适配（设备像素比）
  const dpr = Math.min(2, window.devicePixelRatio || 1);
  canvas.width = Math.floor(canvas.width * dpr);
  canvas.height = Math.floor(canvas.height * dpr);
  ctx.scale(dpr, dpr);

  // DOM refs
  const hpFill = document.getElementById('hpFill');
  const xpFill = document.getElementById('xpFill');
  const lvText = document.getElementById('lv');
  const scoreText = document.getElementById('score');
  const waveNo = document.getElementById('waveNo');
  const leftText = document.getElementById('left');
  const coopTag = document.getElementById('coopTag');
  const toast = document.getElementById('toast');
  const upgradeModal = document.getElementById('upgradeModal');
  const choicesWrap = document.getElementById('choices');
  const gameOver = document.getElementById('gameOver');
  const finalScore = document.getElementById('finalScore');
  const restartBtn = document.getElementById('restartBtn');
  const skA = document.getElementById('skA');
  const skB = document.getElementById('skB');

  const CONFIG = {
    width: 960, height: 600,
    player: { size: 22, color: '#4CAF50', speed: 220, baseHp: 120, pickRange: 70 },
    bullet: { speed: 520, size: 4, baseDamage: 12 },
    skillA: { cd: 0.28 },
    skillB: { cd: 2.6, baseRadius: 110, baseDamage: 28 },
    coop: { range: 120, dmgMul: 1.5, radiusMul: 1.2 },
    wave: { baseCount: 6, grow: 1.25, breakSec: 6 },
    monsters: {
      hpPerLevel: 22,
      chaser: { speed: 80, color: '#FF6B6B' },
      shooter: { speed: 60, color: '#F7B267', shootCd: [1.2, 2.0], bulletSpeed: 280, bulletDamage: 10, keepDist: 160 },
      dasher:  { speed: 70, color: '#9878FF', dashSpeed: 280, dashTime: 0.35, cooldown: [2.6,4.0] }
    },
    xp: { perOrb: [5,9], levelCurve: 50, growth: 1.25 },
    sfxVol: 0.15
  };

  const rand = (a,b) => Math.random()*(b-a)+a;
  const clamp = (v,min,max)=>Math.max(min,Math.min(max,v));
  const dist = (ax,ay,bx,by)=>Math.hypot(ax-bx, ay-by);

  const game = {
    time: 0,
    state: 'playing', // 'playing', 'upgrade', 'over'
    score: 0,
    coop: false,
    player: null,
    otherPlayers: [],
    bullets: [],
    mBullets: [],
    monsters: [],
    orbs: [],
    particles: [],
    effects: [],
    skills: { A:{cdLeft:0}, B:{cdLeft:0} },
    input: { dragging:false, sx:0, sy:0, px:0, py:0, clickCount:0, clickTimer:null, moved:false },
    wave: { no:1, inBreak:false, breakLeft:0, left:0, spawnQueue:[] },
    shake: { t:0, mag:0 },
    audio: { ctx:null }
  };

  // 初始化
  function init() {
    game.score = 0;
    game.state = 'playing';
    game.particles.length = 0;
    game.effects.length = 0;
    game.mBullets.length = 0;
    game.bullets.length = 0;
    game.monsters.length = 0;
    game.orbs.length = 0;
    game.otherPlayers = [];
    game.skills.A.cdLeft = 0;
    game.skills.B.cdLeft = 0;
    game.coop = false;
    game.wave.no = 1;
    game.wave.inBreak = false;
    game.wave.breakLeft = 0;
    game.wave.spawnQueue = [];
    game.wave.left = 0;

    game.player = {
      x: CONFIG.width/2, y: CONFIG.height/2, size: CONFIG.player.size,
      color: CONFIG.player.color,
      hp: CONFIG.player.baseHp, maxHp: CONFIG.player.baseHp,
      level: 1, xp: 0, xpTo: CONFIG.xp.levelCurve,
      speed: CONFIG.player.speed,
      dmgMul: 1.0,
      pickRange: CONFIG.player.pickRange
    };

    // AI 队友
    for (let i=0;i<2;i++){
      game.otherPlayers.push({
        x: rand(80, CONFIG.width-80),
        y: rand(80, CONFIG.height-80),
        size: 20, color:'#2196F3',
        dir: rand(0,Math.PI*2), t: rand(0,60)
      });
    }

    spawnWave(true);
    updateHUD();
    hide(gameOver);
    hide(upgradeModal);
  }

  // 波次
  function spawnWave(first=false) {
    const count = Math.floor(CONFIG.wave.baseCount * Math.pow(CONFIG.wave.grow, game.wave.no-1));
    game.wave.left = count;
    game.wave.spawnQueue.length = 0;
    for (let i=0;i<count;i++){
      const type = pickMonsterTypeForWave(game.wave.no);
      game.wave.spawnQueue.push({ type, t: i * 0.35 }); // 逐个刷入
    }
    showToast(`第 ${game.wave.no} 波`, 900);
    if(!first) screenShake(6, 180);
  }

  function pickMonsterTypeForWave(no) {
    if (no <= 1) return 'chaser';
    const r = Math.random();
    if (no < 3) return r<0.7?'chaser':'shooter';
    if (no < 5) return r<0.55?'chaser':(r<0.8?'shooter':'dasher');
    return r<0.5?'chaser':(r<0.8?'shooter':'dasher');
  }

  function spawnMonster(type){
    // 从边缘刷
    let x, y;
    if (Math.random()<0.5){
      x = Math.random()<0.5 ? -20 : CONFIG.width+20;
      y = rand(20, CONFIG.height-20);
    } else {
      x = rand(20, CONFIG.width-20);
      y = Math.random()<0.5 ? -20 : CONFIG.height+20;
    }
    const level = clamp(Math.floor(rand(game.player.level-1, game.player.level+2)),1, 99);
    const size = 16 + level*2.2;
    const hp = level * CONFIG.monsters.hpPerLevel;
    const base = { x,y,size, level, hp, maxHp:hp, hit:0, color:'#f55', type, vx:0, vy:0, t:0 };
    if (type==='chaser') {
      base.color = CONFIG.monsters.chaser.color;
      base.speed = CONFIG.monsters.chaser.speed + level*3;
    } else if (type==='shooter') {
      base.color = CONFIG.monsters.shooter.color;
      base.speed = CONFIG.monsters.shooter.speed + level*2;
      base.shootCd = rand(...CONFIG.monsters.shooter.shootCd);
    } else if (type==='dasher') {
      base.color = CONFIG.monsters.dasher.color;
      base.speed = CONFIG.monsters.dasher.speed + level*2;
      base.dashCd = rand(...CONFIG.monsters.dasher.cooldown);
      base.dashing = 0;
    }
    game.monsters.push(base);
  }

  // 输入
  canvas.addEventListener('mousedown', (e)=>pointerDown(e));
  canvas.addEventListener('mousemove', (e)=>pointerMove(e));
  canvas.addEventListener('mouseup',   (e)=>pointerUp(e));
  canvas.addEventListener('mouseleave',()=>{ game.input.dragging=false; });

  canvas.addEventListener('touchstart',(e)=>{ e.preventDefault(); pointerDown(e.touches[0]); },{passive:false});
  canvas.addEventListener('touchmove', (e)=>{ e.preventDefault(); pointerMove(e.touches[0]); },{passive:false});
  canvas.addEventListener('touchend',  (e)=>{ e.preventDefault(); pointerUp({clientX:game.input.px, clientY:game.input.py}); },{passive:false});

  restartBtn.addEventListener('click', ()=>{ init(); });

  function localPos(e){
    const rect = canvas.getBoundingClientRect();
    const scaleX = canvas.width/dpr/rect.width;
    const scaleY = canvas.height/dpr/rect.height;
    return { x:(e.clientX-rect.left)*scaleX, y:(e.clientY-rect.top)*scaleY };
  }

  function pointerDown(e){
    if (game.state==='over') return;
    ensureAudio();
    const p = localPos(e);
    game.input.dragging = true;
    game.input.sx = p.x; game.input.sy = p.y;
    game.input.px = p.x; game.input.py = p.y;
    game.input.moved = false;
  }
  function pointerMove(e){
    if (!game.input.dragging) return;
    const p = localPos(e);
    const dx = p.x - game.input.sx;
    const dy = p.y - game.input.sy;
    if (dx*dx + dy*dy > 25) game.input.moved = true; // 移动阈值
    game.player.x = clamp(game.player.x + dx, game.player.size/2, CONFIG.width - game.player.size/2);
    game.player.y = clamp(game.player.y + dy, game.player.size/2, CONFIG.height - game.player.size/2);
    game.input.sx = p.x; game.input.sy = p.y;
    game.input.px = p.x; game.input.py = p.y;
  }
  function pointerUp(e){
    if (game.state==='upgrade') return; // 升级暂停期间不触发技能
    if (!game.input.dragging) return;
    game.input.dragging = false;

    // 点击/双击识别：如果没有明显移动，判定点击
    const p = localPos(e);
    const movedDist = Math.hypot(p.x - game.input.px, p.y - game.input.py);
    if (movedDist < 6 && !game.input.moved) {
      game.input.clickCount++;
      if (game.input.clickCount===1) {
        game.input.clickTimer = setTimeout(()=>{
          if (game.skills.A.cdLeft<=0) skillA(p.x, p.y);
          game.input.clickCount = 0;
        }, 230);
      } else if (game.input.clickCount===2) {
        clearTimeout(game.input.clickTimer);
        if (game.skills.B.cdLeft<=0) skillB();
        game.input.clickCount = 0;
      }
    }
  }

  // 技能
  function skillA(tx, ty){
    const angle = Math.atan2(ty - game.player.y, tx - game.player.x);
    const spd = CONFIG.bullet.speed;
    const dmg = CONFIG.bullet.baseDamage * (game.coop ? CONFIG.coop.dmgMul : 1) * game.player.dmgMul;
    game.bullets.push({ x:game.player.x, y:game.player.y, vx:Math.cos(angle)*spd, vy:Math.sin(angle)*spd, size:CONFIG.bullet.size, dmg, life: 2.2 });
    game.skills.A.cdLeft = CONFIG.skillA.cd;
    sfxShoot();
  }

  function skillB(){
    const radius = CONFIG.skillB.baseRadius * (game.coop? CONFIG.coop.radiusMul : 1);
    const dmg = CONFIG.skillB.baseDamage * (game.coop ? CONFIG.coop.dmgMul : 1) * game.player.dmgMul;
    game.effects.push({ x:game.player.x, y:game.player.y, r:0, max:radius, dmg });
    game.skills.B.cdLeft = CONFIG.skillB.cd;

    // 立即对范围内怪造成伤害
    for (let m of game.monsters) {
      const d = dist(game.player.x, game.player.y, m.x, m.y);
      if (d < radius) {
        hurtMonster(m, dmg);
      }
    }
    screenShake(8, 200);
    sfxExplosion();
  }

  // 音效
  function ensureAudio(){
    if (game.audio.ctx) return;
    try {
      game.audio.ctx = new (window.AudioContext || window.webkitAudioContext)();
    } catch {}
  }
  function tone(freq=440, dur=0.08, type='square', gain=CONFIG.sfxVol){
    if (!game.audio.ctx) return;
    const ctxA = game.audio.ctx;
    const o = ctxA.createOscillator();
    const g = ctxA.createGain();
    o.type = type; o.frequency.value = freq;
    g.gain.value = gain;
    o.connect(g).connect(ctxA.destination);
    o.start();
    // 快速衰减
    g.gain.exponentialRampToValueAtTime(0.001, ctxA.currentTime + dur);
    o.stop(ctxA.currentTime + dur + 0.02);
  }
  const sfxShoot = ()=>tone(520, 0.06, 'square', CONFIG.sfxVol*0.8);
  const sfxHit   = ()=>tone(220, 0.05, 'sawtooth', CONFIG.sfxVol*0.5);
  const sfxExplosion = ()=>tone(120, 0.16, 'sawtooth', CONFIG.sfxVol*0.9);
  const sfxLevelUp   = ()=>{ tone(660,0.08,'triangle',CONFIG.sfxVol*0.8); setTimeout(()=>tone(880,0.12,'triangle',CONFIG.sfxVol*0.8), 80); };

  // 升级
  const UPGRADES = [
    { name:'强力子弹', desc:'伤害 +25%', apply: p => p.dmgMul*=1.25 },
    { name:'生命强化', desc:'最大生命 +25%（并回满）', apply: p => { p.maxHp=Math.round(p.maxHp*1.25); p.hp=p.maxHp; } },
    { name:'灵活身法', desc:'移动速度 +15%', apply: p => p.speed*=1.15 },
    { name:'技能训练A', desc:'技能A冷却 -20%', apply: p => CONFIG.skillA.cd*=0.8 },
    { name:'技能训练B', desc:'技能B冷却 -15%', apply: p => CONFIG.skillB.cd*=0.85 },
    { name:'范围专精', desc:'范围技能半径 +20%', apply: p => CONFIG.skillB.baseRadius*=1.2 },
    { name:'吸附范围', desc:'拾取范围 +60%', apply: p => p.pickRange*=1.6 },
    { name:'恢复体质', desc:'每波结束回复 30% 生命', apply: p => p.waveRegen = true },
  ];

  function levelUp(){
    game.player.level++;
    lvText.textContent = game.player.level;
    showToast('LV UP!', 900);
    sfxLevelUp();
    // 升级选择
    const choices = [];
    const used = new Set();
    while (choices.length<3) {
      const i = Math.floor(Math.random()*UPGRADES.length);
      if (!used.has(i)) { used.add(i); choices.push(UPGRADES[i]); }
    }
    showUpgradeChoices(choices);
  }

  function showUpgradeChoices(list){
    game.state = 'upgrade';
    upgradeModal.style.display = 'grid';
    choicesWrap.innerHTML = '';
    list.forEach((u,idx)=>{
      const div = document.createElement('div');
      div.className = 'choice';
      div.innerHTML = `<div class="t">${u.name}</div><div style="font-size:13px; opacity:0.85">${u.desc}</div>`;
      div.addEventListener('click', ()=>{
        u.apply(game.player);
        hide(upgradeModal);
        game.state = 'playing';
      });
      choicesWrap.appendChild(div);
    });
  }

  function onMonsterDead(m){
    // 掉经验球
    const n = Math.floor(rand(2,4));
    for (let i=0;i<n;i++){
      const val = Math.floor(rand(...CONFIG.xp.perOrb)) * (1 + Math.floor(m.level/3));
      const ang = rand(0, Math.PI*2);
      const sp = rand(30, 80);
      game.orbs.push({ x:m.x, y:m.y, vx:Math.cos(ang)*sp, vy:Math.sin(ang)*sp, val, t:0 });
    }
    game.score += 10 + m.level*3;
    game.wave.left = Math.max(0, game.wave.left-1);
    sfxHit();
  }

  function pickOrb(o){
    game.player.xp += o.val;
    showFloatingText(`+${o.val} XP`, o.x, o.y, '#90EE90');
    // 升级判断
    while (game.player.xp >= game.player.xpTo) {
      game.player.xp -= game.player.xpTo;
      game.player.xpTo = Math.floor(game.player.xpTo * CONFIG.xp.growth);
      levelUp();
    }
  }

  // 伤害/飘字/粒子
  function hurtMonster(m, dmg){
    m.hp -= dmg;
    m.hit = 0.12;
    showFloatingText(`-${Math.floor(dmg)}`, m.x, m.y- m.size/2, '#ffdede');
    for (let i=0;i<6;i++) {
      const ang = rand(0,Math.PI*2), sp = rand(40,120);
      game.particles.push({ x:m.x, y:m.y, vx:Math.cos(ang)*sp, vy:Math.sin(ang)*sp, r:2, life: rand(0.2,0.5), color:'#ffb0a8' });
    }
    if (m.hp<=0){
      onMonsterDead(m);
    }
  }

  function showFloatingText(t, x, y, color='#fff'){
    game.particles.push({ x, y, vx: rand(-10,10), vy: -50, text:t, life:0.9, color });
  }

  function showToast(text, ms=1000){
    toast.textContent = text;
    toast.classList.add('show');
    setTimeout(()=>toast.classList.remove('show'), ms);
  }

  function screenShake(mag=6, dur=150){
    game.shake.mag = mag;
    game.shake.t = dur/1000;
  }

  function endGame(){
    game.state = 'over';
    finalScore.textContent = game.score;
    gameOver.style.display = 'grid';
  }

  function hide(el){ el.style.display='none'; }

  // 更新
  let last = performance.now();
  function loop(now){
    const dt = Math.min(0.033, (now - last)/1000);
    last = now;
    if (game.state==='playing') {
      update(dt);
    }
    render(dt);
    requestAnimationFrame(loop);
  }

  function update(dt){
    game.time += dt;

    // 冷却
    game.skills.A.cdLeft = Math.max(0, game.skills.A.cdLeft - dt);
    game.skills.B.cdLeft = Math.max(0, game.skills.B.cdLeft - dt);

    // AI 队友移动和偶尔射击
    for (let a of game.otherPlayers){
      a.t += dt;
      if (a.t > 1) { a.t = 0; a.dir += rand(-0.6,0.6); }
      a.x = clamp(a.x + Math.cos(a.dir)*70*dt, 10, CONFIG.width-10);
      a.y = clamp(a.y + Math.sin(a.dir)*70*dt, 10, CONFIG.height-10);
      // 偶尔朝最近怪开火（装饰效果，子弹不造成伤害，可做粒子）
      if (Math.random()<0.01 && game.monsters.length){
        const m = game.monsters[Math.floor(Math.random()*game.monsters.length)];
        if (m) {
          // 小粒子线提示
          game.particles.push({ x:a.x, y:a.y, vx:(m.x-a.x)*2, vy:(m.y-a.y)*2, r:1, life:0.12, color:'#8ecaff' });
        }
      }
    }

    // 协同加成
    game.coop = game.otherPlayers.some(o => dist(o.x,o.y, game.player.x, game.player.y) < CONFIG.coop.range);
    coopTag.style.display = game.coop ? 'block' : 'none';

    // 波次刷怪队列
    if (!game.wave.inBreak) {
      if (game.wave.spawnQueue.length){
        // 减少每项延迟
        for (let i=0;i<game.wave.spawnQueue.length;i++){
          game.wave.spawnQueue[i].t -= dt;
        }
        // 取出应刷的
        while (game.wave.spawnQueue.length && game.wave.spawnQueue[0].t <= 0){
          const it = game.wave.spawnQueue.shift();
          spawnMonster(it.type);
        }
      } else if (game.wave.left <= 0 && game.monsters.length===0) {
        // 波结束
        game.wave.inBreak = true;
        game.wave.breakLeft = CONFIG.wave.breakSec;
        showToast('整备时间', 800);
        // 恢复体质天赋
        if (game.player.waveRegen) {
          const heal = Math.floor(game.player.maxHp*0.3);
          game.player.hp = Math.min(game.player.maxHp, game.player.hp + heal);
          showFloatingText(`+${heal} HP`, game.player.x, game.player.y-20, '#a0ffa0');
        }
      }
    } else {
      game.wave.breakLeft -= dt;
      if (game.wave.breakLeft <=0){
        game.wave.inBreak = false;
        game.wave.no++;
        spawnWave();
      }
    }

    // 玩家子弹
    for (let i=game.bullets.length-1;i>=0;i--){
      const b = game.bullets[i];
      b.x += b.vx*dt; b.y += b.vy*dt; b.life -= dt;
      if (b.x< -10 || b.x>CONFIG.width+10 || b.y< -10 || b.y>CONFIG.height+10 || b.life<=0) {
        game.bullets.splice(i,1); continue;
      }
      // 碰撞怪物
      for (let j=game.monsters.length-1;j>=0;j--){
        const m = game.monsters[j];
        if (dist(b.x,b.y,m.x,m.y) < (m.size/2 + b.size)) {
          hurtMonster(m, b.dmg);
          game.bullets.splice(i,1);
          if (m.hp<=0) game.monsters.splice(j,1);
          break;
        }
      }
    }

    // 怪物逻辑
    for (let i=game.monsters.length-1;i>=0;i--){
      const m = game.monsters[i];
      m.t += dt;
      if (m.hit>0) m.hit -= dt;

      if (m.type==='chaser'){
        const ang = Math.atan2(game.player.y - m.y, game.player.x - m.x);
        m.x += Math.cos(ang)*m.speed*dt;
        m.y += Math.sin(ang)*m.speed*dt;
      } else if (m.type==='shooter'){
        const d = dist(m.x,m.y, game.player.x, game.player.y);
        const keep = CONFIG.monsters.shooter.keepDist;
        if (d < keep-6){
          const ang = Math.atan2(m.y - game.player.y, m.x - game.player.x);
          m.x += Math.cos(ang)*m.speed*dt;
          m.y += Math.sin(ang)*m.speed*dt;
        } else if (d > keep+6){
          const ang = Math.atan2(game.player.y - m.y, game.player.x - m.x);
          m.x += Math.cos(ang)*m.speed*dt;
          m.y += Math.sin(ang)*m.speed*dt;
        }
        // 射击
        m.shootCd -= dt;
        if (m.shootCd <=0){
          m.shootCd = rand(...CONFIG.monsters.shooter.shootCd);
          const a = Math.atan2(game.player.y - m.y, game.player.x - m.x);
          const sp = CONFIG.monsters.shooter.bulletSpeed;
          game.mBullets.push({ x:m.x, y:m.y, vx:Math.cos(a)*sp, vy:Math.sin(a)*sp, size:4, dmg: CONFIG.monsters.shooter.bulletDamage });
        }
      } else if (m.type==='dasher'){
        if (m.dashing > 0){
          m.dashing -= dt;
          const a = Math.atan2(game.player.y - m.y, game.player.x - m.x);
          m.x += Math.cos(a)*CONFIG.monsters.dasher.dashSpeed*dt;
          m.y += Math.sin(a)*CONFIG.monsters.dasher.dashSpeed*dt;
        } else {
          const a = Math.atan2(game.player.y - m.y, game.player.x - m.x);
          m.x += Math.cos(a)*m.speed*dt;
          m.y += Math.sin(a)*m.speed*dt;
          m.dashCd -= dt;
          if (m.dashCd<=0){
            m.dashing = CONFIG.monsters.dasher.dashTime;
            m.dashCd = rand(...CONFIG.monsters.dasher.cooldown);
            // 预兆粒子
            for (let k=0;k<8;k++){
              const ang = rand(0,Math.PI*2), sp=rand(30,80);
              game.particles.push({ x:m.x, y:m.y, vx:Math.cos(ang)*sp, vy:Math.sin(ang)*sp, r:1.8, life:0.3, color:'#cbb8ff' });
            }
          }
        }
      }

      // 碰撞玩家（触碰伤害）
      if (dist(m.x,m.y, game.player.x, game.player.y) < (m.size/2 + game.player.size/2)){
        const heavy = (m.level > game.player.level);
        const dmg = heavy ? 35 : 12;
        game.player.hp -= dmg;
        screenShake(5 + (heavy?4:0), 120);
        sfxHit();
        // 击退怪物
        const ang = Math.atan2(m.y - game.player.y, m.x - game.player.x);
        m.x += Math.cos(ang)*24; m.y += Math.sin(ang)*24;
      }

      // 危险连线提示（高等级）
      // 渲染中处理
    }

    // 怪物子弹
    for (let i=game.mBullets.length-1;i>=0;i--){
      const b = game.mBullets[i];
      b.x += b.vx*dt; b.y += b.vy*dt;
      if (b.x< -10 || b.x>CONFIG.width+10 || b.y< -10 || b.y>CONFIG.height+10) {
        game.mBullets.splice(i,1); continue;
      }
      if (dist(b.x,b.y, game.player.x, game.player.y) < (b.size + game.player.size/2)){
        game.player.hp -= b.dmg;
        game.mBullets.splice(i,1);
        screenShake(5, 120);
        sfxHit();
      }
    }

    // 经验球
    for (let i=game.orbs.length-1;i>=0;i--){
      const o = game.orbs[i];
      o.t += dt;
      // 漂散减速
      o.vx *= 0.98; o.vy *= 0.98;
      o.x += o.vx*dt; o.y += o.vy*dt;
      const d = dist(o.x,o.y, game.player.x, game.player.y);
      if (d< game.player.pickRange){
        // 吸附
        const a = Math.atan2(game.player.y - o.y, game.player.x - o.x);
        const sp = clamp(240 - d, 120, 360);
        o.x += Math.cos(a)*sp*dt; o.y += Math.sin(a)*sp*dt;
      }
      if (d < game.player.size/2 + 6){
        pickOrb(o);
        game.orbs.splice(i,1);
      }
    }

    // 粒子 & 特效
    for (let i=game.particles.length-1;i>=0;i--){
      const p = game.particles[i];
      p.life -= dt;
      if (p.life<=0){ game.particles.splice(i,1); continue; }
      if (p.text){
        p.x += p.vx*dt; p.y += p.vy*dt;
      } else {
        p.vy += 20*dt; // 重力微弱
        p.x += p.vx*dt; p.y += p.vy*dt;
        p.r = Math.max(0, p.r - dt*2);
      }
    }
    for (let i=game.effects.length-1;i>=0;i--){
      const e = game.effects[i];
      e.r += 180*dt;
      if (e.r>e.max) game.effects.splice(i,1);
    }

    // 屏幕震动时间
    if (game.shake.t>0) game.shake.t -= dt;

    // 玩家死亡
    if (game.player.hp<=0) {
      endGame();
    }

    updateHUD();
  }

  // HUD 更新
  function updateHUD(){
    hpFill.style.width = `${clamp(game.player.hp/game.player.maxHp, 0, 1)*100}%`;
    xpFill.style.width = `${clamp(game.player.xp/game.player.xpTo, 0, 1)*100}%`;
    scoreText.textContent = game.score;
    waveNo.textContent = game.wave.no;
    leftText.textContent = game.wave.left + (game.wave.inBreak? `（休整 ${Math.ceil(game.wave.breakLeft)}s）` : '');

    // 技能圆形冷却
    updateSkillUI(skA, game.skills.A.cdLeft, CONFIG.skillA.cd);
    updateSkillUI(skB, game.skills.B.cdLeft, CONFIG.skillB.cd);
  }

  function updateSkillUI(el, left, total){
    const cd = el.querySelector('.cd');
    const sec = el.querySelector('.sec');
    if (left>0 && total>0){
      const p = clamp(left/total, 0, 1);
      cd.style.display = 'block';
      cd.style.background = `conic-gradient(#ff695e ${p*360}deg, rgba(255,255,255,0.08) 0)`;
      sec.textContent = Math.ceil(left);
    } else {
      cd.style.display = 'none';
      sec.textContent = '';
    }
  }

  // 渲染
  function render(dt){
    // 背景网格
    ctx.clearRect(0,0,CONFIG.width, CONFIG.height);
    ctx.save();
    // 屏幕震动
    if (game.shake.t>0){
      const k = game.shake.t;
      const mag = game.shake.mag * k;
      ctx.translate(rand(-mag,mag), rand(-mag,mag));
    }

    drawBackgroundGrid();

    // 合作范围圈
    if (game.coop) {
      ctx.strokeStyle = 'rgba(255,210,74,0.18)'; ctx.lineWidth=2;
      ctx.beginPath();
      ctx.arc(game.player.x, game.player.y, CONFIG.coop.range, 0, Math.PI*2);
      ctx.stroke();
    }

    // 特效先画
    for (let e of game.effects){
      const a = clamp(1 - e.r/e.max, 0, 1);
      ctx.strokeStyle = `rgba(255,140,80,${0.6*a})`;
      ctx.lineWidth = 4;
      ctx.beginPath();
      ctx.arc(e.x, e.y, e.r, 0, Math.PI*2);
      ctx.stroke();
    }

    // 其他玩家
    for (let a of game.otherPlayers){
      drawBox(a.x, a.y, a.size, '#2196F3', 0);
      drawLabel('玩家', a.x, a.y - a.size/2 - 6, 10, 'rgba(255,255,255,0.9)');
    }

    // 危险连接线
    for (let m of game.monsters){
      if (m.level > game.player.level){
        const d = dist(m.x,m.y, game.player.x, game.player.y);
        if (d < 180){
          ctx.strokeStyle = 'rgba(255,60,60,0.5)';
          ctx.lineWidth = 2;
          ctx.beginPath(); ctx.moveTo(game.player.x, game.player.y); ctx.lineTo(m.x, m.y); ctx.stroke();
          drawLabel('危险!', (game.player.x+m.x)/2, (game.player.y+m.y)/2, 12, 'rgba(255,80,80,0.9)');
        }
      }
    }

    // 怪物
    for (let m of game.monsters){
      const c = m.hit>0 ? '#fff' : m.color;
      drawBox(m.x, m.y, m.size, c, 0);
      // 血条
      const bw = m.size, bh=4, y = m.y - m.size/2 - 9;
      ctx.fillStyle = 'rgba(255,80,80,0.8)';
      ctx.fillRect(m.x - bw/2, y, bw, bh);
      ctx.fillStyle = '#4caf50';
      ctx.fillRect(m.x - bw/2, y, bw * clamp(m.hp/m.maxHp,0,1), bh);
      drawLabel('Lv'+m.level, m.x, m.y+4, 12);
    }

    // 怪物子弹
    for (let b of game.mBullets){
      ctx.fillStyle = '#ffa94d';
      ctx.beginPath(); ctx.arc(b.x, b.y, b.size, 0, Math.PI*2); ctx.fill();
    }

    // 玩家子弹
    for (let b of game.bullets){
      ctx.fillStyle = game.coop ? '#FFD700' : '#ffffff';
      ctx.beginPath(); ctx.arc(b.x, b.y, b.size, 0, Math.PI*2); ctx.fill();
    }

    // 经验球
    for (let o of game.orbs){
      ctx.fillStyle = '#95f7a5';
      ctx.beginPath(); ctx.arc(o.x, o.y, 4, 0, Math.PI*2); ctx.fill();
      ctx.strokeStyle = 'rgba(149,247,165,0.25)'; ctx.lineWidth = 1;
      ctx.beginPath(); ctx.arc(o.x, o.y, 7, 0, Math.PI*2); ctx.stroke();
    }

    // 玩家
    drawBox(game.player.x, game.player.y, game.player.size, game.player.color, 0);
    drawLabel('Lv'+game.player.level, game.player.x, game.player.y+5, 12);

    // 粒子和飘字
    for (let p of game.particles){
      if (p.text){
        drawLabel(p.text, p.x, p.y, 14, p.color);
      } else {
        ctx.fillStyle = p.color || 'rgba(255,255,255,0.8)';
        ctx.beginPath(); ctx.arc(p.x, p.y, Math.max(0,p.r), 0, Math.PI*2); ctx.fill();
      }
    }

    ctx.restore();
  }

  function drawBackgroundGrid(){
    const gap = 40, w = CONFIG.width, h=CONFIG.height;
    ctx.fillStyle = '#1f1f1f';
    ctx.fillRect(0,0,w,h);
    ctx.strokeStyle = 'rgba(255,255,255,0.05)';
    ctx.lineWidth = 1;
    for (let x=0; x<=w; x+=gap){
      ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,h); ctx.stroke();
    }
    for (let y=0; y<=h; y+=gap){
      ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(w,y); ctx.stroke();
    }
  }

  function drawBox(x,y,size,color){
    ctx.fillStyle = color;
    ctx.fillRect(x - size/2, y - size/2, size, size);
  }
  function drawLabel(text, x,y, size=12, color='rgba(255,255,255,0.95)'){
    ctx.fillStyle = color; ctx.font = `bold ${size}px Arial`; ctx.textAlign='center';
    ctx.fillText(text, x, y);
  }

  // 启动
  init();
  requestAnimationFrame(loop);

  // 调整 UI 宽度时重新定位（可选）
  window.addEventListener('resize', ()=>{ /* 这里保持canvas固定大小，UI自适应 */ });

})();
</script>
</body>
</html>